﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security;
using System.Security.Principal;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using Netfraction.Common.DataStorage;
using Netfraction.Common.Logging;

namespace NetfractionHub.Security
{
	public class IdentityManager
	{
		internal const string IDENTITIES_DATABASE_NAME = "identities.db";
		internal const string IDENTITIES_TABLE_NAME = "Identities";
		internal const string IDENTITY_PERMISSIONS_TABLE_NAME = "IdentityPermissions";

		private static SqlDataStore _identityStore;
		private List<Identity> _identities;
		private static bool _isPopulated;

		private IdentityManager()
		{
			_identities = new List<Identity>();
			_isPopulated = false;
		}

		private static bool CheckStoreExists()
		{
			if (_identityStore != null)
				return true;

			_identityStore = new SqlDataStore(SecurityDataSource.Source, IdentityManager.IDENTITIES_DATABASE_NAME);

			if (_identityStore == null)
				return false;

			return true;
		}

		private static bool CheckStoreIsPopulated()
		{
			if (!CheckStoreExists())
				return false;

			if (!_identityStore.TableExists(IDENTITIES_TABLE_NAME) || !_identityStore.TableExists(IDENTITY_PERMISSIONS_TABLE_NAME))
				PopulateIdentityStore();

			return true;
		}


		#region Singleton Pattern
		// Use singleton pattern.
		private static IdentityManager _instance;

		/// <summary>
		/// Obtain singleton instance of IdentityManager
		/// </summary>
		/// <returns></returns>
		public static IdentityManager GetInstance()
		{
			// Check instance
			if (IdentityManager._instance == null)
			{
				// Initialise instance
				IdentityManager._instance = new IdentityManager();
			}

			// Return instance
			return IdentityManager._instance;
		}
		#endregion Singleton Pattern

		private static Dictionary<int, Dictionary<Type, AccessState>> identityPermissionCache = null;
		private static Dictionary<int, AccessPermissionBase> accessPermissionCache = null;

		public static void LoadIdentities()
		{
			_isPopulated = CheckStoreIsPopulated();
			if (!_isPopulated)
				return;

			LogManager.WriteEntry("Loading Identities from: " + _identityStore.FullPath, LogEventId.LoadingIdentities, LogLevel.Basic);



			Identity newIdentity = null;
			using (Community.CsharpSqlite.SQLiteClient.SqliteDataReader sdr = _identityStore.ExecuteQuery("SELECT IdentityId, RoleId, ClientId, Name, Password FROM Identities"))
			{
				if (sdr != null)
				{
					while (sdr.Read())
					{
						newIdentity = Identity.LoadIdentity(sdr);
						if (newIdentity != null)
							_instance._identities.Add(newIdentity);
					}
				}
				//_identityStore.Disconnect();
			}
		}

		private static void PopulateIdentityStore()
		{
			if (!CheckStoreExists())
				_identityStore = new SqlDataStore(SecurityDataSource.Source, IDENTITIES_DATABASE_NAME);

			if (!_identityStore.TableExists(IDENTITIES_TABLE_NAME))
			{
				// IF NOT EXISTS is redundant. We wrap this block in the if-statement to ensure these types always exists.
				_identityStore.ExecuteNonQuery(string.Format("CREATE TABLE IF NOT EXISTS {0} (IdentityId INTEGER PRIMARY KEY AUTOINCREMENT, RoleId INTEGER, ClientId TEXT, Name TEXT, Password TEXT)", IDENTITIES_TABLE_NAME));

				_identityStore.ExecuteNonQuery(string.Format(System.Globalization.CultureInfo.InvariantCulture,
					"INSERT INTO {0} (RoleId, ClientId, Name) VALUES ('{1}', '{2}', '{3}')", IDENTITIES_TABLE_NAME, "2", "R2UWZEGET5LR3FGLEVPKGYL5ASMW33XZ6AC5YLY", "SecurityBot"));
				_identityStore.ExecuteNonQuery(string.Format(System.Globalization.CultureInfo.InvariantCulture,
					"INSERT INTO {0} (RoleId, ClientId, Name) VALUES ('{1}', '{2}', '{3}')", IDENTITIES_TABLE_NAME, "2", "O6R5EGD4IU2HRWCBBHU3L2L2O77EMF3JAOJHJOI", "OperatorBot"));
			}
			if (!_identityStore.TableExists(IDENTITY_PERMISSIONS_TABLE_NAME))
			{
				_identityStore.ExecuteNonQuery(string.Format("CREATE TABLE IF NOT EXISTS {0} (IdentityId INTEGER, PermissionId INTEGER, AccessState INTEGER)", IDENTITY_PERMISSIONS_TABLE_NAME));
				/*_identityStore.ExecuteNonQuery(string.Format(System.Globalization.CultureInfo.InvariantCulture,
					"INSERT INTO {0} (IdentityId, PermissionId, AccessState) VALUES ({1}, {2}, {3})", IDENTITY_PERMISSIONS_TABLE_NAME, "1", "OperatorBot", (int)AccessState.Allow));*/
			}

			LogManager.WriteEntry("Creating Identities", LogEventId.CreatingIdentities, LogLevel.Detailed);

			int curIdentityId = GetIdentityIdByName("SecurityBot");
			SetIdentityAllowPermission(_identityStore, curIdentityId, typeof(Permissions.SecurityBotPermission));
		}

		public static void SaveIdentities()
		{
			// should be automatically persisted with the sql database
		}

		private static void SetIdentityAllowPermission(SqlDataStore _sqlStore, int identityId, List<Type> permissionTypes)
		{
			foreach (Type permType in permissionTypes)
			{
				SetIdentityPermission(_sqlStore, identityId, permType, AccessState.Allow);
			}
		}

		private static void SetIdentityDenyPermission(SqlDataStore _sqlStore, int identityId, List<Type> permissionTypes)
		{
			foreach (Type permType in permissionTypes)
			{
				SetIdentityPermission(_sqlStore, identityId, permType, AccessState.Deny);
			}
		}

		private static void SetIdentityAllowPermission(SqlDataStore _sqlStore, int identityId, Type permissionType)
		{
			SetIdentityPermission(_sqlStore, identityId, permissionType, AccessState.Allow);
		}

		private static void SetIdentityDenyPermission(SqlDataStore _sqlStore, int identityId, Type permissionType)
		{
			SetIdentityPermission(_sqlStore, identityId, permissionType, AccessState.Deny);
		}

		private static void SetIdentityPermission(SqlDataStore _sqlStore, int identityId, List<Type> permissionTypes, AccessState state)
		{
			foreach (Type permType in permissionTypes)
			{
				SetIdentityPermission(_sqlStore, identityId, permType, state);
			}
		}

		private static void SetIdentityPermission(SqlDataStore _sqlStore, int identityId, Type permissionType, AccessState state)
		{
			try
			{
				int permissionId = AccessPermissionBase.GetPermissionId(permissionType);
				if (permissionId > 0)
					SetIdentityPermissionInternal(_sqlStore, identityId, permissionId, state);
			}
			catch (Exception e)
			{
				Console.WriteLine(e.Message);
			}
		}

		private static int SetIdentityPermissionInternal(SqlDataStore _sqlStore, int identityId, int permissionId, AccessState state)
		{
			// check if '' should be used around INTEGER values
			object retVal = _sqlStore.ExecuteScalar(String.Format(System.Globalization.CultureInfo.InvariantCulture,
				"SELECT count() FROM {0} WHERE IdentityId={1} AND PermissionId={2}",
				IDENTITY_PERMISSIONS_TABLE_NAME,
				identityId,
				permissionId));

			if (retVal != null && (retVal is Int64 || retVal is Int32))
			{
				if ((retVal is Int64 ? SqlDataStore.CastInt64ToInt((Int64)retVal) : (int)retVal) == 0)
				{
					// use INSERT
					return _sqlStore.ExecuteNonQuery(String.Format(System.Globalization.CultureInfo.InvariantCulture,
						"INSERT INTO {0} (IdentityId, PermissionId, AccessState) VALUES ({1}, {2}, {3})",
						IDENTITY_PERMISSIONS_TABLE_NAME,
						identityId,
						permissionId,
						(int)state));
				}
				else
				{
					// use UPDATE
					return _sqlStore.ExecuteNonQuery(String.Format(System.Globalization.CultureInfo.InvariantCulture,
						"UPDATE {0} SET AccessState={1} WHERE IdentityId={2} AND PermissionId={3})",
						IDENTITY_PERMISSIONS_TABLE_NAME,
						(int)state,
						identityId,
						permissionId));
				}
			}

			return 0;
		}

		private static int GetIdentityIdByName(string identityName)
		{
			object retVal = _identityStore.ExecuteScalar(String.Format(System.Globalization.CultureInfo.InvariantCulture,
			   "SELECT IdentityId FROM {0} WHERE Name='{1}'",
			   IDENTITIES_TABLE_NAME,
			   identityName));

			if (retVal != null && (retVal is Int64 || retVal is Int32))
				return retVal is Int64 ? SqlDataStore.CastInt64ToInt((Int64)retVal) : (int)retVal;
			else
				return 0;
		}



		public static bool LoadIdentityPermissions()
		{
			if (!_isPopulated)
				return false;

			accessPermissionCache = AccessPermissionBase.LoadPermissions();
			identityPermissionCache = new Dictionary<int, Dictionary<Type, AccessState>>();
			using (Community.CsharpSqlite.SQLiteClient.SqliteDataReader sdr = _identityStore.ExecuteQuery(
				string.Format("SELECT PermissionId, AccessState, IdentityId FROM {0} ORDER BY IdentityId", IDENTITY_PERMISSIONS_TABLE_NAME)))
			{
				if (sdr != null)
				{
					while (sdr.Read())
					{
						int identityId = sdr.GetInt32(2);
						if (identityPermissionCache.ContainsKey(identityId))
							identityPermissionCache[identityId].Add(accessPermissionCache[sdr.GetInt32(0)].GetType(), (AccessState)sdr.GetInt32(1));
						else
							identityPermissionCache.Add(identityId, new Dictionary<Type, AccessState> 
								{ 
									{ accessPermissionCache[sdr.GetInt32(0)].GetType(), (AccessState)sdr.GetInt32(1) } 
								});
					}
				}
			}

			return true;
		}

		public static Dictionary<Type, AccessState> GetIdentityPermissionsByIdentityId(int identityId)
		{
			if (identityPermissionCache == null && !LoadIdentityPermissions())
				return null;

			return identityPermissionCache.ContainsKey(identityId) ? identityPermissionCache[identityId] : new Dictionary<Type, AccessState>();
		}

		public static Identity GetIdentityByClientId(string clientId)
		{
			var idents = _instance._identities.Where(p => p.ClientId == clientId);
			if (idents.Count() == 1)
				return idents.First() as Identity;
			else
				return null;
		}

		public static Identity GetIdentityByName(string name)
		{
			var idents = _instance._identities.Where(p => p.Name == name);
			// not sure if it should return an exception or something if there's more than one
			if (idents.Count() > 0)
				return idents.First() as Identity;
			else
				return null;
		}

		public static bool RegisterIdentity(Identity ident)
		{
			if (!_instance._identities.Contains(ident))
			{
				_instance._identities.Add(ident);
				return true;
			}
			return false;
		}

		public static bool UpdateClientId(string name, Identity newIdentity)
		{
			if (name != newIdentity.Name)
				return false;

			var idents = _instance._identities.Where(p => p.Name == name);
			if (idents.Count() == 0)
				return false;

			_instance._identities.Remove(idents.First());
			_instance._identities.Add(newIdentity);
			return true;
		}

		public List<Identity> Identities
		{
			get { return _identities; }
			set { _identities = value; }
		}

		public static void PersistRole(Identity identity)
		{
			if (!CheckStoreExists())
				_identityStore = new SqlDataStore(SecurityDataSource.Source, IDENTITIES_DATABASE_NAME);

			//if (_identityStore.TableExists(IDENTITIES_TABLE_NAME))
			//{
			try
			{
				if (SetIdentityRoleInternal(_identityStore, identity.ClientId, identity.Role.RoleId) > 0)
					_instance._identities.Add(identity);
			}
			catch (Exception ex)
			{
				Console.WriteLine(ex.Message);
				Console.WriteLine(ex.StackTrace);
			}
			//}
		}

		private static int SetIdentityRoleInternal(SqlDataStore _sqlStore, string clientId, int roleId)
		{
			object retVal = _sqlStore.ExecuteScalar(String.Format(System.Globalization.CultureInfo.InvariantCulture,
				"SELECT count() FROM {0} WHERE ClientId='{1}'",
				IDENTITIES_TABLE_NAME,
				clientId));

			if (retVal != null && (retVal is Int64 || retVal is Int32))
			{
				if ((retVal is Int64 ? SqlDataStore.CastInt64ToInt((Int64)retVal) : (int)retVal) == 0)
				{
					// use INSERT
					return _sqlStore.ExecuteNonQuery(String.Format(System.Globalization.CultureInfo.InvariantCulture,
						"INSERT INTO {0} (RoleId, ClientId) VALUES ({1}, '{2}')",
						IDENTITIES_TABLE_NAME,
						roleId,
						clientId));
				}
				else
				{
					// use UPDATE
					return _sqlStore.ExecuteNonQuery(String.Format(System.Globalization.CultureInfo.InvariantCulture,
						"UPDATE {0} SET RoleId={1} WHERE ClientId='{2}'",
						IDENTITIES_TABLE_NAME,
						roleId,
						clientId));
				}
			}

			return 0;
		}
	}
}